﻿#region Copyright information
// 
// Copyright © 2005-2013 Yongkee Cho. All rights reserved.
// 
// This code is a part of the Biological Object Library and governed under the terms of the
// GNU Lesser General  Public License (LGPL) version 2.1 which accompanies this distribution.
// For more information on the LGPL, please visit http://bol.codeplex.com/license.
// 
// - Filename: Perceptron.cs
// - Author: Yongkee Cho
// - Email: yongkeecho@gmail.com
// - Date Created: 2012-12-08 8:26 AM
// - Last Modified: 2013-01-25 3:59 PM
// 
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using BOL.Linq;
using BOL.Linq.Descriptive;
using BOL.Linq.Distances;

namespace BOL.Algorithms.Classification
{
    public class Perceptron<TClass> : LinearClassifierBase<TClass>, ICloneable
    {
        #region Constructor

        public Perceptron(IDictionary<TClass, double[]> weights)
            : base(weights) { }

        #endregion

        #region ICloneable implementation

        public Perceptron<TClass> Clone()
        {
            return new Perceptron<TClass>(Weights.ToDictionary(x => x.Key, x => x.Value));
        }

        object ICloneable.Clone()
        {
            return Clone();
        }

        #endregion

        #region Public methods

        public static Perceptron<TClass> Train<TSource>(IEnumerable<TSource> source, Func<TSource, TClass> classSelector, Func<TSource, Tuple<double[], double>> instancesSelector, double learningRate)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            if (classSelector == null)
                throw new ArgumentNullException("classSelector");
            if (instancesSelector == null)
                throw new ArgumentNullException("instancesSelector");

            var list = source.GroupBy(classSelector).Select(x => new { Class = x.Key, Instances = x.Select(instancesSelector).ToList() }).ToList();
            var length = list.First().Instances.First().Item1.Length;
            var weights = list.ToDictionary(x => x.Class, x => new double[length + 1]);

            while (true)
            {
                var errorCount = 0;

                // foreach class group,
                foreach (var l in list)
                {
                    var c = l.Class;
                    var count = l.Instances.Count;
                    var sum = 0.0;

                    // foreach instance,
                    foreach (var instance in l.Instances)
                    {
                        var feature = instance.Item1;
                        var output = instance.Item2;

                        // calculates the actual output 
                        var y = weights[c].Dot(feature) + weights[c].Last();
                        var diff = output - y;

                        // adapts weights
                        weights[c] = weights[c].Zip(feature, (w, f) => w + learningRate * diff * f).Append(diff).ToArray();
                        sum += diff;
                    }

                    if (sum / count > 1e-10)
                        errorCount++;
                }

                if (errorCount == 0)
                    break;
            }

            return new Perceptron<TClass>(weights);
        }

        public override TClass Classify(double[] features)
        {
            if (features == null)
                throw new ArgumentNullException("features");
            if (features.Length != K)
                throw new ArgumentOutOfRangeException("features");

            // TODO: need to be changed
            // return TrainingSet.ArgMax(x => x.Weight.Dot(features) + x.Weight.Last()).Class;
            return Weights.ToDictionary(x => x.Key, x => x.Value.Dot(features) + x.Value.Last()).ArgMax();
        }

        #endregion
    }
}